
import sys
import serial
import serial.tools.list_ports
from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QComboBox, QPushButton, QVBoxLayout, QWidget, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QFileDialog, QButtonGroup
from PyQt6.QtCore import QThread, pyqtSignal

from PyQt6.QtGui import QPalette, QColor, QFont
import os
import psutil
import wmi
import socket
import tftpy
from PyQt6 import uic #BURA

tftpy_path = os.path.dirname(tftpy.__file__)
print(tftpy_path)
class SerialReaderThread(QThread):
    data_received = pyqtSignal(bytes)
    connection_status = pyqtSignal(bool)
    error_acquired = pyqtSignal(str)

    def __init__(self, port, baudrate):
        super().__init__()
        self.port = port
        self.baudrate = baudrate
        self.serial_port = None
        self.running = True

    def run(self):
        try:
            self.serial_port = serial.Serial(self.port, self.baudrate,timeout=0.0001)
            self.connection_status.emit(True)
            while self.running:
                data = b''
                while True:
                    chunk = self.serial_port.read(2048)
                    if not chunk:
                        break
                    data += chunk
                if data:
                    self.data_received.emit(data)
        except serial.SerialException as e:
            self.connection_status.emit(False)
            self.error_acquired.emit(f'Serial error: {e}')
        except Exception as e:
            self.error_acquired.emit(f'Serial error: {e}')

    def stop(self):
        self.running = False
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()

    def write_data(self, data):
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.write(data)

class SerialMonitor(QMainWindow):
    def __init__(self):
        super().__init__()
        uic.loadUi('ui/main_window.ui', self)  #BURA
        
        self.isfolder = True
        self.theme=0
        self.isreading=False
        self.apply_light_palette()
        
        self.serial_thread = None
        
        self.browse_btn.clicked.connect(self.browse_command)
        self.write_btn.clicked.connect(self.write_command)
        self.write_btn_2.clicked.connect(self.send_bin_file)
        self.read_btn.clicked.connect(self.read_command)
        self.browse_btn_3.clicked.connect(self.browse_command2)
        self.list_network_interfaces()
        self.refresh_ports()
        self.baudrate_selector.setText('115200')
        self.start_button.clicked.connect(self.toggle_reading)

        self.refresh_button.clicked.connect(self.refresh_ports)
        self.browse_folder_btn.clicked.connect(self.browse_folder)
        self.toggle_theme_button.clicked.connect(self.toggle_theme)
        self.send_data_line.returnPressed.connect(self.send_data)
        self.radio_button1.toggled.connect(self.on_radio_button_toggled)
        self.radio_button2.toggled.connect(self.on_radio_button_toggled)
        self.title_label.setStyleSheet('color: #ffffff;')
        self.title_label.setFont(QFont("Montserrat",16))
        self.file_name="file_path.txt"
        self.folder_error=False
        with open(self.file_name,'r') as file:
            data=file.read()
            
        if data.strip():
            self.local_file.setText(data)
            self.local_file_2.setText(data)
    
    def on_radio_button_toggled(self):
        # Check which radio button is selected and act accordingly
        if self.radio_button1.isChecked():
            self.isfolder=False
            print("a")
        elif self.radio_button2.isChecked():
            self.isfolder=True
            print("a")
            
            
    def send_data(self):
        if self.serial_thread is not None:
            data = self.send_data_line.text().encode()
            if self.crCheckBox.isChecked():
                data += b'\r'
            if self.lfCheckBox.isChecked():
                data += b'\n'

            self.serial_thread.write_data(data)
    
    def start_reading(self):
        if self.serial_thread is not None:
            self.serial_thread.stop()
        port = self.port_selector.currentText()
        try:
            baudrate = int(self.baudrate_selector.text())
        except Exception:
            QMessageBox.critical(self, 'Input Error', 'Baudrate must be an integer.')
            return
        else:
            self.serial_thread = SerialReaderThread(port, baudrate)
            self.serial_thread.data_received.connect(self.display_data)
            self.serial_thread.start()

    

    def stop_reading(self):
        if self.serial_thread is not None:
            self.serial_thread.stop()
            self.serial_thread = None
        self.start_button.setEnabled(True)
        
        
    def toggle_reading(self):
        
            if self.isreading:
                self.stop_reading()
                self.start_button.setText("Connect")
                self.isreading=False
            else:
                self.start_reading()
                self.start_button.setText("Disconnect")
                self.isreading=True
            
            
    def display_data(self, data):
        text = data.decode().strip()
        self.textEdit.append(text)
    
    def refresh_ports(self):
        self.port_selector.clear()
        self.port_selector.addItems(self.list_serial_ports())

    def list_serial_ports(self):
        ports = serial.tools.list_ports.comports()
        sorted_ports = sorted((port.device for port in ports))
        return sorted_ports

    def browse_command(self):
        self.isfolder = False
        file_name, _ = QFileDialog.getOpenFileName(self, 'Open File')
        if file_name:
            self.local_file.setText(file_name)
            with open(self.file_name,"w") as file:
                file.write(file_name)

    def browse_command2(self):
        self.isfolder = False
        file_name, _ = QFileDialog.getOpenFileName(self, 'Open File')
        if file_name:
            self.local_file_3.setText(file_name)
            with open(self.file_name,"w") as file:
                file.write(file_name)

    def send_bin_file(self):
        if self.serial_thread and self.serial_thread.serial_port and self.serial_thread.serial_port.is_open:
            try:
                with open(self.local_file_3.text(), 'rb') as file:
                    data = file.read()
                    for i in range(0, len(data), 2):
                        if i + 1 < len(data):
                            formatted_data = bytes([data[i + 1], data[i]])
                        else:
                            formatted_data = bytes([data[i]])
                        self.serial_thread.write_data(formatted_data)
            except Exception as e:
                print(e)

    def browse_folder(self):
        self.isfolder = True
        folder_path = QFileDialog.getExistingDirectory(self, 'Select Folder')
        if folder_path:
            self.local_file_2.setText(folder_path)
            with open(self.file_name,"w") as file:
                file.write(folder_path)

    def custom_upload(self, client, remote_file_name, file_name,isfolder):
        """Custom upload function to track progress."""
        file_size = os.path.getsize(file_name)
        self.progress_bar.setValue(0)
        bytes_sent = 0

        def progress_callback(packet):
            try:
                nonlocal bytes_sent
                bytes_sent += len(packet.data)
              
                progress = int((bytes_sent / file_size) * 100)
               
                self.progress_bar.setValue(progress)
                
            except Exception as e:
                pass
        try:
            with open(file_name, 'rb') as file:
                client.upload(remote_file_name, file, packethook=progress_callback)
            self.progress_bar.setValue(100)
            if isfolder:
                QMessageBox.information(self, 'Success', 'File successfully written!')
                self.progress_bar.setValue(0)
        except Exception as e:
            self.error=True
            QMessageBox.critical(self, 'Error', f'Failed to write file: {str(e)}')
            self.progress_bar.setValue(0)

    def write_command(self):
        if self.isfolder:
            self.write_folder()
        else:
            server_address = self.lineEdit.text()
            port = int(self.port.text())  # Port needs to be an integer
            client = tftpy.TftpClient(server_address, port)

            file_name = self.local_file.text()
            alt_filename = ''
            print(file_name)
            if not os.path.isfile(file_name):
                QMessageBox.critical(self, 'Error', f'File {file_name} does not exist!')
                return
            
            remote_file_name = os.path.basename(file_name) if len(alt_filename) == 0 else alt_filename
            self.custom_upload(client, remote_file_name, file_name,1)

    def write_folder(self):
        server_address = self.lineEdit.text()
        port = int(self.port.text())  # Port needs to be an integer
        client = tftpy.TftpClient(server_address, port)

        folder_path = self.local_file_2.text()
        if not os.path.isdir(folder_path):
            QMessageBox.critical(self, 'Error', f'Folder {folder_path} does not exist or is invalid!')
            return

        files_written = []
        try:
            for root, _, files in os.walk(folder_path):
                for file in files:
                    file_path = os.path.join(root, file)
                    if os.path.isfile(file_path):
                        remote_file_name = os.path.relpath(file_path, folder_path)  # Preserve folder structure
                        self.custom_upload(client, remote_file_name, file_path,0)
                        files_written.append(file_path)
            if not self.folder_error:
                QMessageBox.information(self, 'Success', f'{len(files_written)} files successfully written!')
        except Exception as e:
            self.folder_error=True
            QMessageBox.critical(self, 'Error', f'Failed to write files: {str(e)}')

    def read_command(self):
        server_address = self.host.text()
        port = int(self.port.text())  # Port needs to be an integer
        client = tftpy.TftpClient(server_address, port)

        file_name = self.remote_file.text()
        alt_filename = self.alt_filename.text()
        local_file_name = file_name if len(alt_filename) == 0 else alt_filename

        try:
            client.download(file_name, local_file_name)
            QMessageBox.information(self, 'Success', 'File successfully read!')
        except Exception as e:
            QMessageBox.critical(self, 'Error', f'Failed to read file: {str(e)}')


    def apply_light_palette(self):
        self.headerlayout.setStyleSheet('background-color: #0c4670;')
        palette = QPalette()
        palette.setColor(QPalette.ColorRole.Window, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.WindowText, QColor(0, 0, 0))
        palette.setColor(QPalette.ColorRole.Base, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.AlternateBase, QColor(240, 240, 240))
        palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(0, 0, 0))
        palette.setColor(QPalette.ColorRole.ToolTipText, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.Text, QColor(0, 0, 0))
        palette.setColor(QPalette.ColorRole.Button, QColor(230, 230, 230))
        palette.setColor(QPalette.ColorRole.ButtonText, QColor(0, 0, 0))
        palette.setColor(QPalette.ColorRole.BrightText, QColor(255, 0, 0))
        palette.setColor(QPalette.ColorRole.Highlight, QColor(0, 120, 215))
        palette.setColor(QPalette.ColorRole.HighlightedText, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(150, 150, 150))
        QApplication.instance().setPalette(palette)
        with open('assets/light_mode.qss', 'r') as file:
            self.setStyleSheet(file.read())

    def toggle_theme(self):
        if self.theme == 0:
            self.theme = 1
            self.apply_dark_palette()
            
        else:  # inserted
            self.theme = 0
            self.apply_light_palette()
            

    def apply_dark_palette(self):
        self.headerlayout.setStyleSheet('background-color: #3c3f41;')
        palette = QPalette()
        palette.setColor(QPalette.ColorRole.Window, QColor(125, 125, 125))
        palette.setColor(QPalette.ColorRole.WindowText, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.Base, QColor(80, 80, 80))
        palette.setColor(QPalette.ColorRole.AlternateBase, QColor(102, 102, 102))
        palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.ToolTipText, QColor(0, 0, 0))
        palette.setColor(QPalette.ColorRole.Text, QColor(0, 255, 0))
        palette.setColor(QPalette.ColorRole.Button, QColor(80, 80, 80))
        palette.setColor(QPalette.ColorRole.ButtonText, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.BrightText, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.Highlight, QColor(85, 170, 255))
        palette.setColor(QPalette.ColorRole.HighlightedText, QColor(0, 0, 0))
        palette.setColor(QPalette.ColorRole.Text, QColor(255, 255, 255))
        palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(120, 120, 120))
        QApplication.instance().setPalette(palette)
        with open('assets/dark_mode.qss', 'r') as file:
            self.setStyleSheet(file.read())

    def list_network_interfaces(self):
        self.adresses = []
        use_int = {}
        c = wmi.WMI()
        addr_changed = False
        interfaces = psutil.net_if_addrs()
        macs = {}
        for adapter in c.Win32_NetworkAdapter():
            if adapter.MACAddress:
                wmi_mac = adapter.MACAddress.replace(':', '-')
                macs[adapter.Name] = wmi_mac
        for interface, addrs in interfaces.items():
            addr_changed = True
            for addr in addrs:
                if addr.family == socket.AF_INET:
                    address_name = addr.address
                    if addr_changed == True:
                        use_int[address_name] = interface
                    else:  # inserted
                        use_int[address_name] = add_name
                else:  # inserted
                    if addr.family == psutil.AF_LINK:
                        for key in macs:
                            if addr.address == macs[key]:
                                add_name = key
                                addr_changed = False
        font = QFont('Consolas')
        self.hostComboBox.setFont(font)
        max_ip_length = max((len(ip) for ip in use_int))
    
        for ip, name in use_int.items():
            self.adresses.append([ip, name])
            aligned_text = f'{ip:<{int(max_ip_length)}}   {name}'
            self.hostComboBox.addItem(aligned_text)
            
if __name__ == '__main__':
    app = QApplication(sys.argv)
    monitor = SerialMonitor()
    app.setStyle('Fusion')
    monitor.show()
    sys.exit(app.exec())